# 数据缓存

`cache` 函数可以让你缓存数据获取或计算的结果，它提供了更精细的数据粒度控制，并且适用于客户端渲染(CSR)、服务端渲染（SSR）、API 服务(BFF)等多种场景。

## 基本用法

```ts
import { cache } from '@module-federation/bridge-react';

export type Data = {
  data: string;
};

export const fetchData = cache(async (): Promise<Data> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        data: `[ provider ] fetched data: ${new Date()}`,
      });
    }, 1000);
  });
});

```

### 参数

- `fetchData`: DataLoader 中的 fetchData 函数。
- `options`（可选）: 缓存配置
  - `tag`: 用于标识缓存的标签，可以基于这个标签使缓存失效
  - `maxAge`: 缓存的有效期 (毫秒)
  - `revalidate`: 重新验证缓存的时间窗口（毫秒），与 HTTP Cache-Control 的 stale-while-revalidate 功能一致
  - `getKey`: 简化的缓存键生成函数，根据函数参数生成缓存键
  - `onCache`: 缓存数据时的回调函数，用于自定义缓存数据的处理逻辑
`options` 参数的类型如下：

```ts
interface CacheOptions {
  tag?: string | string[];
  maxAge?: number;
  revalidate?: number;
  getKey?: <Args extends any[]>(...args: Args) => string;
  onCache?: (info: CacheStatsInfo) => boolean;
}
```

### 返回值

`cache` 函数会返回一个新的 `fetchData` 函数，该函数有缓存的能力，多次调用该函数，不会重复执行。

## 使用范围

仅支持在 DataLoader 中使用。

## 详细用法

### `maxAge`

每次计算完成后，框架会记录写入缓存的时间，当再次调用该函数时，会根据 `maxAge` 参数判断缓存是否过期，如果过期，则重新执行 `fn` 函数，否则返回缓存的数据。

```ts
import { cache, CacheTime } from '@module-federation/bridge-react';

const getDashboardStats = cache(
  async () => {
    return await fetchComplexStatistics();
  },
  {
    maxAge: CacheTime.MINUTE * 2,  // 在 2 分钟内调用该函数会返回缓存的数据
  }
);
```

#### `revalidate`

`revalidate` 参数用于设置缓存过期后，重新验证缓存的时间窗口，可以和 `maxAge` 参数一起使用，类似与 HTTP Cache-Control 的 stale-while-revalidate 模式。


如以下示例，在缓存未过期的 2分钟内，如果调用 `getDashboardStats` 函数，会返回缓存的数据，如果缓存过期，2分到3分钟内，收到的请求会先返回旧数据，然后后台会重新请求数据，并更新缓存。

```ts
import { cache, CacheTime } from '@module-federation/bridge-react';

const getDashboardStats = cache(
  async () => {
    return await fetchComplexStatistics();
  },
  {
    maxAge: CacheTime.MINUTE * 2,
    revalidate: CacheTime.MINUTE * 1,
  }
);
```

#### `tag`

`tag` 参数用于标识缓存的标签，可以传入一个字符串或字符串数组，可以基于这个标签使缓存失效，多个缓存函数可以使用一个标签。

```ts
import { cache, revalidateTag } from '@module-federation/bridge-react';

const getDashboardStats = cache(
  async () => {
    return await fetchDashboardStats();
  },
  {
    tag: 'dashboard',
  }
);

const getComplexStatistics = cache(
  async () => {
    return await fetchComplexStatistics();
  },
  {
    tag: 'dashboard',
  }
);

revalidateTag('dashboard-stats'); // 会使 getDashboardStats 函数和 getComplexStatistics 函数的缓存都失效
```

#### `getKey`

`getKey` 参数用于自定义缓存键的生成方式，例如你可能只需要依赖函数参数的一部分来区分缓存。它是一个函数，接收与原始函数相同的参数，返回一个字符串作为缓存键：

```ts
import { cache, CacheTime } from '@module-federation/bridge-react';
import { fetchUserData } from './api';

const getUser = cache(
  async (userId, options) => {
    // 这里 options 可能包含很多配置，但我们只想根据 userId 缓存
    return await fetchUserData(userId, options);
  },
  {
    maxAge: CacheTime.MINUTE * 5,
    // 只使用第一个参数（userId）作为缓存键
    getKey: (userId, options) => userId,
  }
);

// 下面两次调用会共享缓存，因为 getKey 只使用了 userId
await getUser(123, { language: 'zh' });
await getUser(123, { language: 'en' }); // 命中缓存，不会重新请求

// 不同的 userId 会使用不同的缓存
await getUser(456, { language: 'zh' }); // 不会命中缓存，会重新请求
```

你也可以使用 Modern.js 提供的 `generateKey` 函数配合 getKey 生成缓存的键：

:::info

Modern.js 中的 `generateKey` 函数确保即使对象属性顺序发生变化，也能生成一致的唯一键值，保证稳定的缓存

:::

```ts
import { cache, CacheTime, generateKey } from '@module-federation/bridge-react';
import { fetchUserData } from './api';

const getUser = cache(
  async (userId, options) => {
    return await fetchUserData(userId, options);
  },
  {
    maxAge: CacheTime.MINUTE * 5,
    getKey: (userId, options) => generateKey(userId),
  }
);
```


#### `customKey`

`customKey` 参数用于定制缓存的键，它是一个函数，接收一个包含以下属性的对象，返回值必须是字符串或 Symbol 类型，将作为缓存的键：

- `params`：调用缓存函数时传入的参数数组
- `fn`：原始被缓存的函数引用
- `generatedKey`：框架基于入参自动生成的原始缓存键

:::info

一般在以下场景，缓存会失效：
1. 缓存的函数引用发生变化
2. 函数的入参发生变化
3. 不满足 maxAge
4. 调用了 `revalidateTag`

`customKey` 可以用在函数引用不同，但希望共享缓存的场景，如果只是自定义缓存键，推荐使用 `getKey`

:::

这在某些场景下非常有用，比如当函数引用发生变化时，但你希望仍然返回缓存的数据。

```ts
import { cache } from '@module-federation/bridge-react';
import { fetchUserData } from './api';

// 不同的函数引用，但是通过 customKey 可以使它们共享一个缓存
const getUserA = cache(
  fetchUserData,
  {
    maxAge: CacheTime.MINUTE * 5,
    customKey: ({ params }) => {
      // 返回一个稳定的字符串作为缓存的键
      return `user-${params[0]}`;
    },
  }
);

// 即使函数引用变了，只要 customKey 返回相同的值，也会命中缓存
const getUserB = cache(
  (...args) => fetchUserData(...args), // 新的函数引用
  {
    maxAge: CacheTime.MINUTE * 5,
    customKey: ({ params }) => {
      // 返回与 getUserA 相同的键
      return `user-${params[0]}`;
    },
  }
);

// 即使 getUserA 和 getUserB 是不同的函数引用，但由于它们的 customKey 返回相同的值
// 所以当调用参数相同时，它们会共享缓存
const dataA = await getUserA(1);
const dataB = await getUserB(1); // 这里会命中缓存，不会再次发起请求

// 也可以使用 Symbol 作为缓存键（通常用于共享同一个应用内的缓存）
const USER_CACHE_KEY = Symbol('user-cache');
const getUserC = cache(
  fetchUserData,
  {
    maxAge: CacheTime.MINUTE * 5,
    customKey: () => USER_CACHE_KEY,
  }
);

// 可以利用 generatedKey 参数在默认键的基础上进行修改
const getUserD = cache(
  fetchUserData,
  {
    customKey: ({ generatedKey }) => `prefix-${generatedKey}`,
  }
);
```

#### `onCache`

`onCache` 参数允许你跟踪缓存统计信息，例如命中率。这是一个回调函数，接收有关每次缓存操作的信息，包括状态、键、参数和结果。你可以在 onCache 返回 `false` 来阻止命中缓存。

```ts
import { cache, CacheTime } from '@module-federation/bridge-react';

// 跟踪缓存统计
const stats = {
  total: 0,
  hits: 0,
  misses: 0,
  stales: 0,
  hitRate: () => stats.hits / stats.total
};

const getUser = cache(
  fetchUserData,
  {
    maxAge: CacheTime.MINUTE * 5,
    onCache({ status, key, params, result }) {
      // status 可以是 'hit'、'miss' 或 'stale'
      stats.total++;

      if (status === 'hit') {
        stats.hits++;
      } else if (status === 'miss') {
        stats.misses++;
      } else if (status === 'stale') {
        stats.stales++;
      }

      console.log(`缓存${status === 'hit' ? '命中' : status === 'miss' ? '未命中' : '陈旧'}，键：${String(key)}`);
      console.log(`当前命中率：${stats.hitRate() * 100}%`);
    }
  }
);

// 使用示例
await getUser(1); // 缓存未命中
await getUser(1); // 缓存命中
await getUser(2); // 缓存未命中
```

`onCache` 回调接收一个包含以下属性的对象：

- `status`: 缓存操作状态，可以是：
  - `hit`: 缓存命中，返回缓存内容
  - `miss`: 缓存未命中，执行函数并缓存结果
  - `stale`: 缓存命中但数据陈旧，返回缓存内容同时在后台重新验证
- `key`: 缓存键，可能是 `customKey` 的结果或默认生成的键
- `params`: 传递给缓存函数的参数
- `result`: 结果数据（来自缓存或新计算的）

这个回调只在提供 `options` 参数时被调用。当使用无 options 的缓存函数时，不会调用 `onCache` 回调。

`onCache` 回调对以下场景非常有用：
- 监控缓存性能
- 计算命中率
- 记录缓存操作
- 实现自定义指标

### 存储

目前不管是客户端还是服务端，缓存都存储在内存中，默认情况下所有缓存函数共享的存储上限是 1GB，当达到存储上限后，使用 LRU 算法移除旧的缓存。

:::info
考虑到 `cache` 函数缓存的结果内容不会很大，所以目前默认都存储在内存中
:::

可以通过 `configureCache` 函数指定缓存的存储上限：

```ts
import { configureCache, CacheSize } from '@module-federation/bridge-react';

configureCache({
  maxSize: CacheSize.MB * 10, // 10MB
});
```

